# Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
# for details. All rights reserved. Use of this source code is governed by a
# BSD-style license that can be found in the LICENSE file.

# Runs tests with old ABI versions and check if the results differ from the
# current results.

import json
import os
import subprocess
import sys
import time
import utils


scriptDir = os.path.dirname(os.path.realpath(__file__))
outDir = os.path.join(scriptDir, '..', 'out', 'ReleaseX64')


# Info about a running test.
class Test:
  def __init__(self, cmd, resultFile, logFile, version):
    self.cmd = cmd  # The test command.
    self.resultFile = resultFile  # The expected location of the result file.
    self.logFile = logFile  # The expected location of the log file.
    self.version = version  # The ABI version, or None.


# Recursively make directories for the path.
def makeDirs(path):
  try:
    os.makedirs(path)
  except OSError:
    pass


# Build a Test object for the given version (or None).
def buildTest(version):
  testDir = os.path.join(
    outDir, 'test%s' % ('' if version is None else str(version)))
  logDir = os.path.join(testDir, 'logs')
  makeDirs(logDir)

  vm_options = ['--enable-interpreter']
  if version is not None:
    vm_options += ['--use-abi-version=%d' % version]
  cmd = [
      'python',
      os.path.join(scriptDir, 'test.py'),
      '--compiler=dartkb',
      '--mode=release',
      '--write-results',
      '--write-logs',
      '--output_directory=%s' % logDir,
      '--vm-options=%s' % ' '.join(vm_options),
      'lib_2',
  ]

  resultFile = os.path.join(logDir, 'results.json')
  logFile = os.path.join(logDir, 'logs.json')
  return Test(cmd, resultFile, logFile, version)


# Build tests for every supported version, and return a list of Test objects.
def buildAllTests():
  abi_version = int(utils.GetAbiVersion())
  oldest_abi_version = int(utils.GetOldestSupportedAbiVersion())
  tests = [buildTest(None)]
  for version in xrange(oldest_abi_version, abi_version + 1):
    tests.append(buildTest(version))
  return tests


# Run all tests, one by one, and wait for them all to complete.
def runAllTests(tests):
  for test in tests:
    print('\n\n\n=== Running tests %s ===' % (
        ('for ABI version %d' % test.version) if test.version is not None else
        ('without an ABI version')))
    print(subprocess.list2cmdline(test.cmd) + '\n\n')
    proc = subprocess.Popen(test.cmd)
    while proc.returncode is None:
      time.sleep(1)
      proc.communicate()
      proc.poll()


# Read a test result file or log file and convert JSON lines to a dictionary of
# JSON records indexed by name. Assumes result and log files both use name key.
def readTestFile(fileName):
  with open(fileName, 'r') as f:
    return { r['name']: r for r in [json.loads(line) for line in f]}


# Read the test result or log files for every version and return a dict like:
# {name: {version: resultJson, ...}, ...}
def readAllTestFiles(tests, nameGetter):
  allRecords = {}
  for test in tests:
    records = readTestFile(nameGetter(test))
    for name, result in records.items():
      if name not in allRecords:
        allRecords[name] = {}
      allRecords[name][test.version] = result
  return allRecords


# Pick any element of the dictionary, favoring the None key if it exists.
def pickOne(d):
  if None in d:
    return d[None]
  for v in d.values():
    return v
  return None


# Diff the results of a test for each version and construct a new test result
# that reports whether the test results match for each version.
def diffResults(results):
  outResult = pickOne(results)
  exp = results[None]['result'] if None in results else None
  outResult['expected'] = exp
  outResult['result'] = exp
  outResult['matches'] = True
  diffs = []
  for version, result in results.items():
    if version is not None:
      act = result['result']
      if exp != act:
        diffs.append(version)
        outResult['result'] = act  # May be overwritten by other versions.
        outResult['matches'] = False
  return outResult, diffs


# Create a log entry for a test that has diffs. Concatenate all the log records
# and include which tests failed.
def makeLog(diffs, results, logRecords):
  result = pickOne(results)
  logs = ["%s: %s" % (str(v), l['log']) for v, l in logRecords.items()]
  return {
    'name': result['name'],
    'configuration': result['configuration'],
    'result': result['result'],
    'log': '\n\n\n'.join([repr(diffs)] + logs),
  }


# Diff the results of all the tests and create the merged result and log files.
def diffAllResults(tests):
  allResults = readAllTestFiles(tests, lambda test: test.resultFile)
  allLogs = readAllTestFiles(tests, lambda test: test.logFile)
  anyDiff = False
  logDir = os.path.join(outDir, 'logs')
  makeDirs(logDir)
  resultFileName = os.path.join(logDir, 'results.json')
  logFileName = os.path.join(logDir, 'logs.json')
  with open(resultFileName, 'w') as resultFile:
    with open(logFileName, 'w') as logFile:
      for name, results in allResults.items():
        outResult, diffs = diffResults(results)
        resultFile.write(json.dumps(outResult) + '\n')
        if diffs:
          logRecords = allLogs[name] if name in allLogs else []
          logFile.write(json.dumps(makeLog(diffs, results, logRecords)) + '\n')
          anyDiff = True
  return anyDiff


def main():
  tests = buildAllTests()
  runAllTests(tests)
  return 1 if diffAllResults(tests) else 0


if __name__ == '__main__':
  sys.exit(main())
